﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;

namespace Luminal
{
    public struct CommandQueueDescription
    {
        public bool EnableOutOfOrderExecution;
        public bool EnableProfiling;

        public CommandQueueDescription(bool enableOutOfOrderExecution, bool enableProfiling)
        {
            EnableOutOfOrderExecution = enableOutOfOrderExecution;
            EnableProfiling = enableProfiling;
        }
    }

    public sealed partial class CommandQueue : LuminalObject
    {
        #region Interop
        [DllImport("OpenCL.dll")]
        static extern IntPtr clCreateCommandQueue(IntPtr context,
            IntPtr device,
            cl_command_queue_properties properties,
            out ErrorCode errcode_ret);

        [DllImport("OpenCL.dll")]
        static extern ErrorCode clReleaseCommandQueue(IntPtr command_queue);

        [DllImport("OpenCL.dll")]
        static extern ErrorCode clGetCommandQueueInfo(IntPtr command_queue,
            cl_command_queue_info param_name,
            SizeT param_value_size,
            IntPtr param_value,
            out SizeT param_value_size_ret);

        [DllImport("OpenCL.dll")]
        static extern ErrorCode clSetCommandQueueProperty(IntPtr command_queue,
            cl_command_queue_properties properties,
            uint enable,
            IntPtr old_properties);

        [DllImport("OpenCL.dll")]
        static extern ErrorCode clEnqueueReadBuffer(IntPtr command_queue,
            IntPtr buffer,
            uint blocking_read,
            SizeT offset,
            SizeT cb,
            IntPtr ptr,
            uint num_events_in_wait_list,
            IntPtr[] event_wait_list,
            out IntPtr @event);

        [DllImport("OpenCL.dll")]
        static extern ErrorCode clEnqueueWriteBuffer(IntPtr command_queue,
            IntPtr buffer,
            uint blocking_write,
            SizeT offset,
            SizeT cb,
            IntPtr ptr,
            uint num_events_in_wait_list,
            IntPtr[] event_wait_list,
            out IntPtr @event);

        [DllImport("OpenCL.dll")]
        static extern ErrorCode clEnqueueCopyBuffer(IntPtr command_queue,
            IntPtr src_buffer,
            IntPtr dst_buffer,
            SizeT src_offset,
            SizeT dst_offset,
            SizeT cb,
            uint num_events_in_wait_list,
            IntPtr[] event_wait_list,
            out IntPtr @event);

        [DllImport("OpenCL.dll")]
        static extern ErrorCode clEnqueueReadImage(IntPtr command_queue,
            IntPtr image,
            uint blocking_read,
            SizeT[] origin,
            SizeT[] region,
            SizeT row_pitch,
            SizeT slice_pitch,
            IntPtr ptr,
            uint num_events_in_wait_list,
            IntPtr[] event_wait_list,
            out IntPtr @event);

        [DllImport("OpenCL.dll")]
        static extern ErrorCode clEnqueueWriteImage(IntPtr command_queue,
            IntPtr image,
            uint blocking_write,
            SizeT[] origin,
            SizeT[] region,
            SizeT input_row_pitch,
            SizeT input_slice_pitch,
            IntPtr ptr,
            uint num_events_in_wait_list,
            IntPtr[] event_wait_list,
            out IntPtr @event);

        [DllImport("OpenCL.dll")]
        static extern ErrorCode clEnqueueCopyImage(IntPtr command_queue,
            IntPtr src_image,
            IntPtr dst_image,
            SizeT[] src_origin,
            SizeT[] dst_origin,
            SizeT[] region,
            uint num_events_in_wait_list,
            IntPtr[] event_wait_list,
            out IntPtr @event);

        [DllImport("OpenCL.dll")]
        static extern ErrorCode clEnqueueNDRangeKernel(IntPtr command_queue,
            IntPtr kernel,
            uint work_dim,
            SizeT[] global_work_offset,
            SizeT[] global_work_size,
            SizeT[] local_work_size,
            uint num_events_in_wait_list,
            IntPtr[] event_wait_list,
            out IntPtr @event);

        [DllImport("OpenCL.dll")]
        static extern ErrorCode clEnqueueMarker(IntPtr command_queue, out IntPtr @event);

        [DllImport("OpenCL.dll")]
        static extern ErrorCode clEnqueueWaitForEvents(IntPtr command_queue, uint num_events, IntPtr[] event_list);

        [DllImport("OpenCL.dll")]
        static extern ErrorCode clEnqueueBarrier(IntPtr command_queue);

        [DllImport("OpenCL.dll")]
        static extern ErrorCode clFlush(IntPtr command_queue);

        [DllImport("OpenCL.dll")]
        static extern ErrorCode clFinish(IntPtr command_queue);
        #endregion

        #region Properties
        public Context Context { get; private set; }
        public DeviceIdentifier Device { get; private set; }

        public bool OutOfOrderExecutionEnabled
        {
            get
            {
                if (Disposed)
                    throw new ObjectDisposedException(GetType().FullName);

                cl_command_queue_properties properties =
                    (cl_command_queue_properties)GetInfo<uint>(cl_command_queue_info.CL_QUEUE_PROPERTIES);

                return (properties & cl_command_queue_properties.CL_QUEUE_OUT_OF_ORDER_EXEC_MODE_ENABLE) == cl_command_queue_properties.CL_QUEUE_OUT_OF_ORDER_EXEC_MODE_ENABLE;
            }
            set
            {
                if (Disposed)
                    throw new ObjectDisposedException(GetType().FullName);
                if (!Device.CommandQueueCapabilities.SupportsOutOfOrderExecution && value)
                    throw new InvalidOperationException("The specified value is not supported by the device.");

                Util.ThrowError(
                    "Could not set property.",
                    clSetCommandQueueProperty(InternalPointer, cl_command_queue_properties.CL_QUEUE_OUT_OF_ORDER_EXEC_MODE_ENABLE, value ? 1u : 0u, IntPtr.Zero));
            }
        }

        public bool ProfilingEnabled
        {
            get
            {
                if (Disposed)
                    throw new ObjectDisposedException(GetType().FullName);

                cl_command_queue_properties properties =
                    (cl_command_queue_properties)GetInfo<uint>(cl_command_queue_info.CL_QUEUE_PROPERTIES);

                return (properties & cl_command_queue_properties.CL_QUEUE_PROFILING_ENABLE) == cl_command_queue_properties.CL_QUEUE_PROFILING_ENABLE;
            }
            set
            {
                if (Disposed)
                    throw new ObjectDisposedException(GetType().FullName);
                if (!Device.CommandQueueCapabilities.SupportsProfiling && value)
                    throw new InvalidOperationException("The specified value is not supported by the device.");

                Util.ThrowError(
                    "Could not set property.",
                    clSetCommandQueueProperty(InternalPointer, cl_command_queue_properties.CL_QUEUE_PROFILING_ENABLE, value ? 1u : 0u, IntPtr.Zero));
            }
        }

        private T GetInfo<T>(cl_command_queue_info info) where T : struct
        {
            SizeT infoSize = 0;
            Util.ThrowError("Could not get device property.", clGetCommandQueueInfo(InternalPointer, info, 0, IntPtr.Zero, out infoSize));

            T output = default(T);
            Util.Pin(ref output, ptr =>
            {
                Util.ThrowError("Could not get device property.", clGetCommandQueueInfo(InternalPointer, info, infoSize, ptr, out infoSize));
            });

            return output;
        }
        #endregion

        #region Constructors
        public CommandQueue(Context context, DeviceIdentifier device, CommandQueueDescription description)
        {
            if (context == null)
                throw new ArgumentNullException("context");
            if (context.Disposed)
                throw new ObjectDisposedException("context");
            if (device == null)
                throw new ArgumentNullException("device");
            if (device.Disposed)
                throw new ObjectDisposedException("device");
            if (device.CommandQueueCapabilities.SupportsOutOfOrderExecution == false && description.EnableOutOfOrderExecution == true)
                throw new ArgumentException("The specified description is not supported by the device.", "description");
            if (device.CommandQueueCapabilities.SupportsProfiling == false && description.EnableProfiling == true)
                throw new ArgumentException("The specified description is not supported by the device.", "description");

            cl_command_queue_properties clProp = cl_command_queue_properties.None;
            if (description.EnableOutOfOrderExecution)
                clProp |= cl_command_queue_properties.CL_QUEUE_OUT_OF_ORDER_EXEC_MODE_ENABLE;
            if (description.EnableProfiling)
                clProp |= cl_command_queue_properties.CL_QUEUE_PROFILING_ENABLE;

            ErrorCode errorCode;
            Initialize(clCreateCommandQueue(context.InternalPointer, device.InternalPointer, clProp, out errorCode));

            try
            {
                Util.ThrowError("Could not create command queue.", errorCode);
            }
            catch (Exception ex)
            {
                Dispose();
                throw ex;
            }

            Context = context;
            Device = device;
        }

        private CommandQueue(IntPtr ptr)
        {
            Initialize(ptr);

            try
            {
                Context = Context.FromPointer(GetInfo<IntPtr>(cl_command_queue_info.CL_QUEUE_CONTEXT));
                Device = DeviceIdentifier.FromPointer(GetInfo<IntPtr>(cl_command_queue_info.CL_QUEUE_DEVICE));
            }
            catch (Exception ex)
            {
                Dispose();
                throw ex;
            }
        }
        #endregion

        #region Flush/Finish
        public void Flush()
        {
            Util.ThrowError("Could not flush commands.", clFlush(InternalPointer));
        }

        public void Finish()
        {
            Util.ThrowError("Could not finish commands.", clFinish(InternalPointer));
        }
        #endregion

        #region Methods
        public Event EnqueueMarker()
        {
            IntPtr evt;
            Util.ThrowError("Could not enqueue marker.", clEnqueueMarker(InternalPointer, out evt));

            return Event.FromHandle(evt);
        }

        public void WaitForEvents(params Event[] events)
        {
            IntPtr[] eventsPtrs = Util.ExtractPointers<Event>(events);
            Util.Pin(events, ptr =>
            {
                Util.ThrowError("Could not enqueue operation.",
                    clEnqueueWaitForEvents(InternalPointer, (uint)events.Length, eventsPtrs));
            });
        }

        public void EnqueueBarrier()
        {
            Util.ThrowError("Could not enqueue barrier.",
                clEnqueueBarrier(InternalPointer));
        }
        #endregion

        protected override void Dispose(bool disposing)
        {
            if (disposing)
                clReleaseCommandQueue(InternalPointer);
            
            base.Dispose(disposing);
        }

        public static CommandQueue FromPointer(IntPtr ptr)
        {
            return ObjectTracker.ContainsObject(ptr) ? ObjectTracker.GetObject<CommandQueue>(ptr) : new CommandQueue(ptr);
        }
    }
}
